﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Agatha.Common;
using Agatha.Common.InversionOfControl;
using Agatha.ServiceLayer;
using Common.Logging;
using QuickNet;
using QuickNet.Generators;
using QuickNet.Generators.Meta;
using QuickNet.Types;
using Rhino.Mocks;
using xunit.extensions.quicknet;

namespace Tests
{
    public class OneWayRequestProcessorSpecs : AcidTest
    {
        #region Context

        // ---------------------------
        // Todo make this dynamic
        
        public class FirstRequest : OneWayRequest { }

        private static IOneWayRequestHandler<FirstRequest> handlerForFirstRequest;

        public class SecondRequest : OneWayRequest { }

        private static IOneWayRequestHandler<SecondRequest> handlerForSecondRequest;

        public class ThirdRequest : OneWayRequest { }

        private static IOneWayRequestHandler<ThirdRequest> handlerForThirdRequest;
        
        // ---------------------------

        private static IRequestProcessor requestProcessor;
        private static ILog logger;
        private static ServiceLayerConfiguration serviceLayerConfiguration;

        private static List<Exception> exceptionsThrown;

        public OneWayRequestProcessorSpecs() : base(10, 10) { }

        public override void SetUp()
        {
            IoC.Container = new Agatha.Castle.Container();

            serviceLayerConfiguration = MockRepository.GenerateStub<ServiceLayerConfiguration>(null, null, IoC.Container);
            serviceLayerConfiguration.BusinessExceptionType = typeof (BusinessException);
            serviceLayerConfiguration.SecurityExceptionType = typeof(SecurityException);

            requestProcessor = new RequestProcessor(serviceLayerConfiguration);

            handlerForFirstRequest = MockRepository.GenerateMock<IOneWayRequestHandler<FirstRequest>>();
            handlerForSecondRequest = MockRepository.GenerateMock<IOneWayRequestHandler<SecondRequest>>();
            handlerForThirdRequest = MockRepository.GenerateMock<IOneWayRequestHandler<ThirdRequest>>();

            IoC.Container.RegisterInstance(handlerForFirstRequest);
            IoC.Container.RegisterInstance(handlerForSecondRequest);
            IoC.Container.RegisterInstance(handlerForThirdRequest);
        }

        public const BindingFlags FlagsForGettingEverything =
              BindingFlags.NonPublic
            | BindingFlags.Public
            | BindingFlags.Instance
            | BindingFlags.FlattenHierarchy;

        public class BusinessException : Exception { }
        public class SecurityException : Exception { }
        public class UnknownException : Exception { }
        public class AnotherUnknownException : Exception { }

        public class ProcessOneWayRequestsInputElement
        {
            public IOneWayRequestHandler RequestHandler { get; private set; }

            public OneWayRequest Request { get; private set; }

            public ProcessOneWayRequestsInputElement(IOneWayRequestHandler requestHandler, OneWayRequest request)
            {
                RequestHandler = requestHandler;
                Request = request;
            }

            public void StubIt(Action<IList<Exception>> action, IList<Exception> exceptionsThrown)
            {
                RequestHandler
                    .Stub(r => r.Handle(Request))
                    .WhenCalled(arg => action(exceptionsThrown))
                    .Repeat.Once();
            }
        }

        public class RequestsAndHandlers : Dictionary<Type, ProcessOneWayRequestsInputElement>
        {
            public RequestsAndHandlers()
            {
                Add(typeof(FirstRequest),
                    new ProcessOneWayRequestsInputElement(handlerForFirstRequest, new FirstRequest()));
                Add(typeof(SecondRequest),
                    new ProcessOneWayRequestsInputElement(handlerForSecondRequest, new SecondRequest()));
                Add(typeof(ThirdRequest),
                    new ProcessOneWayRequestsInputElement(handlerForThirdRequest, new ThirdRequest()));
            }
        }

        public class ProcessOneWayRequestsInput
        {
            public IList<Action<IList<Exception>>> Actions { get; set; }
            public RequestsAndHandlers RequestsAndHandlers { get; set; }
            public OneWayRequest[] Requests
            {
                get
                {
                    return RequestsAndHandlers.ToList().ConvertAll(kv => kv.Value.Request).ToArray();
                }
            }
        }

        public class ProcessOneWayRequestsInputGenerator : BaseGenerator<ProcessOneWayRequestsInput>
        {
            protected override ProcessOneWayRequestsInput GetDefaultInstance()
            {
                return new ProcessOneWayRequestsInput
                    {
                        RequestsAndHandlers = new RequestsAndHandlers()
                    };
            }

            public ProcessOneWayRequestsInputGenerator()
            {
                IGenerator<Action<IList<Exception>>> actionGenerator =
                    new ChoiceGenerator<Action<IList<Exception>>>(
                        new Action<IList<Exception>>[]
                            {
                                // repetition increases likelyhood
                                l => l.Add(null),
                                l => l.Add(null),
                                l => l.Add(null),
                                l => l.Add(null),
                                l =>
                                    {
                                        Exception exception = new BusinessException();
                                        l.Add(exception);
                                        throw exception;
                                    },
                                l =>
                                    {
                                        Exception exception = new SecurityException();
                                        l.Add(exception);
                                        throw exception;
                                    },
                                l =>
                                    {
                                        Exception exception = new UnknownException();
                                        l.Add(exception);
                                        throw exception;
                                    },
                                l =>
                                    {
                                        Exception exception = new AnotherUnknownException();
                                        l.Add(exception);
                                        throw exception;
                                    }
                            });
                AddGeneratorForProperty(
                    t => t.Actions,
                    new ListGenerator<Action<IList<Exception>>>(actionGenerator, 3, 3));
            }
        }
        #endregion

        #region Transitions 

        class ProcessOneWayRequests : MetaTransition<ProcessOneWayRequestsInput, Null>
        {
            public ProcessOneWayRequests()
            {
                Generator = new ProcessOneWayRequestsInputGenerator();
                Execute = 
                    input =>
                        {
                            logger = MockRepository.GenerateMock<ILog>();
                            FieldInfo info = requestProcessor.GetType().GetField("logger", FlagsForGettingEverything);
                            info.SetValue(requestProcessor, logger);

                            int ix = 0;
                            exceptionsThrown = new List<Exception>();
                            foreach (Type key in input.RequestsAndHandlers.Keys)
                            {
                                input.RequestsAndHandlers[key].StubIt(input.Actions[ix], exceptionsThrown);
                                ix++;
                            }
                            requestProcessor.ProcessOneWayRequests(input.Requests);
                            return null;
                        };
                
            }
        }

        #endregion

        [SpecFor(typeof(ProcessOneWayRequests))]
        public Spec ProcessRequestsWithoutException(ProcessOneWayRequestsInput input, Null output)
        {
            return new Spec(
                () => logger.AssertWasNotCalled(l => l.Error(Arg<string>.Is.Anything, Arg<Exception>.Is.Anything)))
                .IfAfter(() => !exceptionsThrown.Exists(e => e != null));
        }

        [SpecFor(typeof(ProcessOneWayRequests))]
        public Spec ProcessRequestsWithBusinessException(ProcessOneWayRequestsInput input, Null output)
        {
            Predicate<Exception> predicate = 
                exception => exception != null && exception.GetType() == typeof(BusinessException);

            return new Spec(
                () =>
                    {
                        int times = exceptionsThrown.FindAll(predicate).Count;
                        logger.AssertWasCalled(
                            l => l.Error(
                                Arg<string>.Matches(arg => arg == "RequestProcessor: unhandled exception while handling request!"),
                                Arg<Exception>.Matches(arg => arg.GetType() == typeof(BusinessException))),
                                options => options.Repeat.Times(times));
                    })
                .IfAfter(() => exceptionsThrown.Exists(predicate));
        }

        [SpecFor(typeof(ProcessOneWayRequests))]
        public Spec ProcessRequestsWithSecurityException(ProcessOneWayRequestsInput input, Null output)
        {
            Predicate<Exception> predicate =
                exception => exception != null && exception.GetType() == typeof(SecurityException);

            return new Spec(
                () =>
                {
                    int times = exceptionsThrown.FindAll(predicate).Count;
                    logger.AssertWasCalled(
                        l => l.Error(
                            Arg<string>.Matches(arg => arg == "RequestProcessor: unhandled exception while handling request!"),
                            Arg<Exception>.Matches(arg => arg.GetType() == typeof(SecurityException))),
                            options => options.Repeat.Times(times));
                })
                .IfAfter(() => exceptionsThrown.Exists(predicate));
        }

        [SpecFor(typeof(ProcessOneWayRequests))]
        public Spec ProcessRequestsWithUnknownException(ProcessOneWayRequestsInput input, Null output)
        {
            Predicate<Exception> predicate =
                exception => exception != null && exception.GetType() == typeof(UnknownException);
            return new Spec(
                () =>
                {
                    int times = exceptionsThrown.FindAll(predicate).Count;
                    logger.AssertWasCalled(
                        l => l.Error(
                            Arg<string>.Matches(arg => arg == "RequestProcessor: unhandled exception while handling request!"),
                            Arg<Exception>.Matches(arg => arg.GetType() == typeof(UnknownException))),
                            options => options.Repeat.Times(times));
                })
                .IfAfter(() => exceptionsThrown.Exists(predicate));
        }

        [SpecFor(typeof(ProcessOneWayRequests))]
        public Spec ProcessRequestsWithAnotherUnknownException(ProcessOneWayRequestsInput input, Null output)
        {
            Predicate<Exception> predicate =
                exception => exception != null && exception.GetType() == typeof(AnotherUnknownException);
            return new Spec(
                () =>
                {
                    int times = exceptionsThrown.FindAll(predicate).Count;
                    logger.AssertWasCalled(
                        l => l.Error(
                            Arg<string>.Matches(arg => arg == "RequestProcessor: unhandled exception while handling request!"),
                            Arg<Exception>.Matches(arg => arg.GetType() == typeof(AnotherUnknownException))),
                            options => options.Repeat.Times(times));
                })
                .IfAfter(() => exceptionsThrown.Exists(predicate));
        }
    }
}
